package org.gwt.grin.client.features;

import java.util.Map;

import org.gwt.grin.client.Feature;
import org.gwt.grin.client.commands.Command;
import org.gwt.grin.client.util.Graphics;
import org.gwt.grin.client.util.Rectangle;

/**
 * An {@code InterpolateModel} is a feature that controls one or more integer
 * factors, and interpolates their values according to keyframes. This is used
 * by other features to animate the parameters in question. At the end of the
 * sequence, the InterpolateModel can kick off a list of commands, it can repeat
 * the animation, or it can stick at the last frame.
 * <p>
 * An {@code InterpolateModel} with no values can function as a timer. A timer
 * simply has a number of keyframes, and triggers a set of commands after those
 * keyframes.
 * 
 * @see Translator
 * 
 * @author Bill Foote (http://jovial.com)
 * @author ggeorg
 */
public class InterpolatedModel extends Feature {

	/** For a scale_model, the field for the X value. */
	public final static int SCALE_X_FIELD = 0;

	/** For a scale_model, the field for the Y value. */
	public final static int SCALE_Y_FIELD = 1;

	/**
	 * For a scale_model, the field for the horizontal scale factor int mills
	 * (1/1000).
	 */
	public final static int SCALE_X_FACTOR_FIELD = 2;

	/**
	 * For a scale_model, the field for the vertical scale factor int mills
	 * (1/1000).
	 */
	public final static int SCALE_Y_FACTOR_FIELD = 3;

	/*
	 * Frames and values are read-only, while currValues are mutable, and
	 * reconstructed using getIntArray.
	 */

	/** Frame number of keyframes, [0] is always 0. */
	protected int[] frames;

	/** Current value of each field. */
	protected int[] currValues;

	/**
	 * Values at keyframe, indexed by field. The array for a field can be
	 * {@code null}, in which case the initial value of currValues will be
	 * maintained.
	 */
	protected int[][] values;

	/**
	 * Frame to go to after the end. {@code Integer.MAX_VALUE} means
	 * "stick at end", {@code 0} will case a cycle.
	 */
	protected int repeatFrame;

	/**
	 * # of times to repeat images before sending end commands,
	 * {@code Integer.MAX_VALUE} means "infinite".
	 */
	protected int loopCount;

	protected boolean dynScaleX = false;
	protected boolean dynScaleY = false;

	private boolean isActivated = false;
	private int currFrame; // current frame in cycle
	private int currIndex; // frames[index] <= currFrame < frames[index+1]
	private int repeatIndex; // index when currFrame is repeatFrame 1
	private int loopsRemaining; // see loopCount

	protected Command[] endCommands;

	public InterpolatedModel(String name) {
		super(name);
	}

	public void setup(int[] frames, int[] currValues, int[][] values,
			int repeatFrame, int loopCount, Command[] endCommands,
			boolean dynScaleX, boolean dynScaleY) {
		this.frames = frames;
		this.currValues = currValues;
		this.values = values;
		this.repeatFrame = repeatFrame;
		this.loopCount = loopCount;
		this.endCommands = endCommands;
		this.dynScaleX = dynScaleX;
		this.dynScaleY = dynScaleY;
	}

	@Override
	protected Feature createClone(Map<Feature, Feature> clones) {
		if (!isSetup() || isActivated) {
			throw new IllegalStateException();
		}
		InterpolatedModel result = new InterpolatedModel(null);
		result.show = show; // FIXME
		result.frames = frames;
		result.currValues = new int[currValues.length];
		System.arraycopy(currValues, 0, result.currValues, 0, currValues.length);
		result.values = values;
		result.repeatFrame = repeatFrame;
		result.loopCount = loopCount;
		result.currFrame = currFrame;
		result.currIndex = currIndex;
		result.repeatIndex = repeatIndex;
		result.loopsRemaining = loopsRemaining;
		return result;
	}

	@Override
	protected void initializeClone(Feature original,
			Map<Feature, Feature> clones) {
		super.initializeClone(original, clones);
		InterpolatedModel other = (InterpolatedModel) original;
		endCommands = Feature.cloneCommands(other.endCommands, clones);
	}

	/**
	 * Give the current value for the given field.
	 * 
	 * @param fieldNum
	 *            the field number, counting from {@code 0}
	 * @return the field value
	 */
	public final int getField(int fieldNum) {
		/*
		 * If this is an automatically generated value, then it only has a
		 * meaningful value if we're activated.
		 */
		assert !(!isActivated && values[fieldNum] != null) : "InterpolatedModel "
				+ getName() + " not activated";

		return currValues[fieldNum];
	}

	/**
	 * Sets the value of a field to {@code value}. This method may be called by
	 * application code, so longas it's called within a command body or inside
	 * of {@code Director.notifyNextFrame()}. It is an error to call this method
	 * for any field that is changed by the normal interpolation scheme, and
	 * trying to do so may result in an assertion failure. In order words, if
	 * you want to programmatically control a value, make sure that every
	 * keyframe has the same(initial) value for that field.
	 * 
	 * @see {@link #SCALE_X_FIELD}
	 * @sew {@link #SCALE_Y_FIELD}
	 * @see {@link #SCALE_X_FACTOR_FIELD}
	 * @see {@link #SCALE_Y_FACTOR_FIELD}
	 * @see {@link Translator#X_FIELD}
	 * @see {@link Translator#Y_FIELD}
	 * 
	 * @param fieldNum
	 *            the field number, counting from {@code 0}
	 * @param value
	 *            the field value
	 */
	public final void setField(int fieldNum, int value) {
		/*
		 * This is a value that is interpolated.
		 */
		assert (values[fieldNum] == null);

		currValues[fieldNum] = value;
	}

	/**
	 * {@inheritDoc}
	 * 
	 * Since and {@code InterpolatedModel} is invisible, this returns
	 * {@link Integer#MAX_VALUE}.
	 * 
	 * @return {@link Integer#MAX_VALUE}
	 */
	@Override
	public int getX() {
		return Integer.MAX_VALUE;
	}

	/**
	 * {@inheritDoc}
	 * 
	 * Since and {@code InterpolatedModel} is invisible, this returns
	 * {@link Integer#MAX_VALUE}.
	 * 
	 * @return {@link Integer#MAX_VALUE}
	 */
	@Override
	public int getY() {
		return Integer.MAX_VALUE;
	}

	/**
	 * Return the list of commands that are executed at the end of doing our
	 * translation.
	 * 
	 * @return a list of commands
	 */
	public Command[] getEndCommands() {
		return endCommands;
	}

	final boolean getIsActivated() {
		return isActivated;
	}

	/**
	 * Initialize this {@link Feature}. This is called on show initialization. A
	 * show will initialize all of its features after it initializes the
	 * segments.
	 */
	@Override
	protected void initialize() {
		if (repeatFrame == Integer.MAX_VALUE) {
			repeatIndex = Integer.MAX_VALUE;
		} else {
			repeatIndex = 0;
			// This is tricky. We must calculate the index such
			// that frames[i] <= (repeatFrame - 1) < frames[i+1]
			while (repeatFrame - 1 >= frames[repeatIndex + 1]) {
				repeatIndex++;
			}
			// now repeatFrame-1 < frames[repeatIndex + 1]
			// and repeatFrame-1 >= frames[repeatIndex]
		}
	}

	@Override
	protected void destroy() {
		// do nothing
	}

	@Override
	protected void setActivateMode(boolean mode) {
		isActivated = mode;
		if (mode) {
			loopsRemaining = loopCount;
			if (frames.length <= 1) {
				currFrame = Integer.MAX_VALUE;
				currIndex = Integer.MAX_VALUE;
			} else {
				currFrame = 0;
				currIndex = 0;
				for (int i = 0; i < currValues.length; i++) {
					if (values[i] != null) {
						currValues[i] = values[i][0];
					}
				}
			}
		}
	}

	@Override
	protected int setSetupMode(boolean mode) {
		// do nothing
		return 0;
	}

	@Override
	public boolean needsMoreSetup() {
		return false;
	}

	@Override
	public void nextFrame() {
		assert isActivated : "InterpolatedModel " + getName()
				+ " not activated";

		if (currFrame == Integer.MAX_VALUE) {
			return;
		}

		currFrame++;
		int nextIndex = currIndex + 1;
		int dist = frames[nextIndex] - frames[currIndex];
		int distNext = frames[nextIndex] - currFrame;
		int distLast = currFrame - frames[currIndex];

		/*
		 * distNext, the distance to the next value, should never be less than
		 * zero, except for one special case. That special case is a one-frame
		 * timer, because the frames array for a one-frame timer contains { 0 ,
		 * 0 }.
		 */
		assert !(distLast < 0 || (distNext < 0 && frames[nextIndex] != 0));

		for (int i = 0; i < currValues.length; i++) {
			int[] vs = values[i];

			if (vs != null) {
				currValues[i] = (vs[nextIndex] * distLast + vs[currIndex]
						* distNext)
						/ dist;
			}
		}

		if (distNext <= 0) {
			// It will be -1 in the case of a one-frame timer
			currIndex = nextIndex;
			if (currIndex + 1 >= frames.length) {
				if (loopCount != Integer.MAX_VALUE) {
					loopsRemaining--;
				}
				if (loopsRemaining > 0) {
					if (repeatFrame == Integer.MAX_VALUE) {
						currFrame = 0;
						currIndex = 0;
					} else {
						currFrame = repeatFrame;
						if (currFrame != Integer.MAX_VALUE) {
							currFrame--;
						}
						currIndex = repeatIndex;
					}
				} else {
					loopsRemaining = loopCount;
					currFrame = repeatFrame;
					if (currFrame != Integer.MAX_VALUE) {
						currFrame--;
					}
					currIndex = repeatIndex;
					show.runCommands(endCommands);
				}
			}
		}
	}

	@Override
	public void markAsChanged() {
		// do nothing
	}

	@Override
	public void paintFrame(Graphics gc) {
		// do nothing
	}

	@Override
	public void paintDone() {
		// do nothing
	}

	/**
	 * Scale the x, y, width and height values according to the current values
	 * of SCALE_X_FIELD, SCALE_Y_FIELD, SCALE_X_FACTOR_FIELD and
	 * SCALE_Y_FACTOR_FIELD, storing them in scaledBounds. This, of course,
	 * depends on this InterpolatedModel having these four values in it, which
	 * means that this is a scaling_model.
	 * 
	 * @return true if the value of scaledBounds have changed, false otherwise.
	 **/
	public boolean scaleBounds(Graphics gc, double x, double y, double width,
			double height, Rectangle scaledBounds) {
		assert isActivated : "InterpolatedModel " + getName()
				+ " not activated";

		double dx = getField(SCALE_X_FIELD);
		double dy = getField(SCALE_Y_FIELD);
		double xf = getField(SCALE_X_FACTOR_FIELD);
		if (dynScaleX) {
			xf *= gc.getClientWidth();
			xf /= 1000.0;
		}
		double yf = getField(SCALE_Y_FACTOR_FIELD);
		if (dynScaleY) {
			yf *= gc.getClientHeight();
			yf /= 1000.0;
		}
		xf /= 1000.0;
		yf /= 1000.0;

		x = (x - dx) * xf;
		x += dx;

		y = (y - dy) * yf;
		y += dy;

		width *= xf;
		height *= yf;

		if (x != scaledBounds.x || y != scaledBounds.y
				|| width != scaledBounds.width || height != scaledBounds.height) {
			scaledBounds.x = (int) x;
			scaledBounds.y = (int) y;
			scaledBounds.width = (int) width;
			scaledBounds.height = (int) height;
			return true;
		} else {
			return false;
		}
	}

}
